package com.jprocms.module.cms.service.accesslog;

import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jpro.framework.tenant.config.TenantProperties;
import com.jpro.framework.tenant.core.service.TenantFrameworkService;
import com.jprocms.module.cms.controller.admin.site.vo.SiteQueryReqVO;
import com.jprocms.module.cms.dal.dataobject.accesslog.AccessLogDO;
import com.jprocms.module.cms.dal.dataobject.accesspage.AccessPageDO;
import com.jprocms.module.cms.dal.dataobject.accessstatistics.AccessStatisticsDO;
import com.jprocms.module.cms.dal.dataobject.accesstrend.AccessTrendDO;
import com.jprocms.module.cms.dal.dataobject.site.SiteDO;
import com.jprocms.module.cms.dal.mysql.accesslog.AccessLogMapper;
import com.jprocms.module.cms.framework.seq.SnowflakeSequence;
import com.jprocms.module.cms.service.accesspage.AccessPageService;
import com.jprocms.module.cms.service.accessstatistics.AccessStatisticsService;
import com.jprocms.module.cms.service.accesstrend.AccessTrendService;
import com.jprocms.module.cms.service.globalconfig.GlobalConfigService;
import com.jprocms.module.cms.service.site.SiteService;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

import static com.jprocms.module.cms.constant.CmsSysConstants.BATCH_SIZE;
import static com.jprocms.module.cms.dal.dataobject.accessstatistics.AccessStatisticsDO.*;

@Component
public class AccessService extends ServiceImpl<AccessLogMapper, AccessLogDO> {


    public void cacheAccessLog(final AccessLogDO bean) {
        visitLogCache.add(bean);
        // 预留100条数据空间，也许可以防止并发插入的数据
        int reservedSize = 100;
        if (visitLogCache.size() >= AccessLogDO.CACHE_SIZE - reservedSize) {
            List<AccessLogDO> list = visitLogCache;
            visitLogCache = Collections.synchronizedList(new LinkedList<>());
            insertVisitLogIfNotBusy(list);
        }
    }

    public void insertVisitLogIfNotBusy(List<AccessLogDO> list) {
        // 是否繁忙，如果繁忙则不执行，丢弃数据，防止负载过大
        if (!IS_INSERT_BUSY.getAndSet(true)) {
            try {
                insertVisitLogAndReleaseBusy(list);
            } catch (Exception e) {
                IS_INSERT_BUSY.set(false);
            }
        }
    }

    @Async
    public void insertVisitLogAndReleaseBusy(List<AccessLogDO> list) {
        doInsertVisitLog(list);
        IS_INSERT_BUSY.set(false);
    }

    public void flushAccessLog() {
        List<AccessLogDO> list = visitLogCache;
        visitLogCache = Collections.synchronizedList(new LinkedList<>());
        insertVisitLog(list);
    }

    @Async
    public void insertVisitLog(List<AccessLogDO> list) {
        doInsertVisitLog(list);
    }

    private void doInsertVisitLog(List<AccessLogDO> list) {
        super.baseMapper.insertBatch(list, BATCH_SIZE);
    }

    public void updateStat() {
        OffsetDateTime now = OffsetDateTime.now();
        OffsetDateTime startOfDay = now.truncatedTo(ChronoUnit.DAYS);
        // 每天的第一分钟，统计昨天的数据
        if (now.compareTo(startOfDay.plusMinutes(1)) < 0) {
            startOfDay = startOfDay.minusDays(1);
        }
        updateVisitStat(AccessStatisticsDO.TYPE_VISITOR, startOfDay);
        updateVisitStat(AccessStatisticsDO.TYPE_SOURCE, startOfDay);
        updateVisitStat(AccessStatisticsDO.TYPE_COUNTRY, startOfDay);
        updateVisitStat(AccessStatisticsDO.TYPE_PROVINCE, startOfDay);
        updateVisitStat(AccessStatisticsDO.TYPE_DEVICE, startOfDay);
        updateVisitStat(AccessStatisticsDO.TYPE_OS, startOfDay);
        updateVisitStat(AccessStatisticsDO.TYPE_BROWSER, startOfDay);
        updateVisitStat(AccessStatisticsDO.TYPE_SOURCE_TYPE, startOfDay);
        updateVisitPageByUrl(startOfDay);
        updateVisitPageByEntryUrl(startOfDay);
        updateVisitTrendByDay(startOfDay);

        // 统计上10分钟数据
        updateVisitTrendByMinute(now.truncatedTo(ChronoUnit.MINUTES).minusMinutes(10));
        OffsetDateTime startOfHour = now.truncatedTo(ChronoUnit.HOURS);
        // 每小时的第一分钟，统计上一个小时的数据
        if (now.compareTo(startOfHour.plusMinutes(1)) < 0) {
            startOfHour = startOfHour.minusHours(1);
        }
        updateVisitTrendByHour(startOfHour);
        OffsetDateTime startOfMonth = startOfDay.withDayOfMonth(1);
        // 每月的第一分钟，统计上一个小时的数据
        if (now.compareTo(startOfMonth.plusMinutes(1)) < 0) {
            startOfMonth = startOfMonth.minusMonths(1);
        }
        updateVisitTrendByMonth(startOfMonth);
    }

    public void updateOnlineVisitors() {
        doUpdateOnlineVisitors();
    }

    private void doUpdateOnlineVisitors() {
        int onlineMinutes = 30;
        final List<SiteDO> siteList = siteService.getSiteList(new SiteQueryReqVO());
        for (SiteDO siteDO : siteList) {
            int onlineVisitors = visitLogService.countVisitors(OffsetDateTime.now().minusMinutes(onlineMinutes), siteDO.getId());
            LambdaUpdateWrapper<SiteDO> updateWrapper = new LambdaUpdateWrapper<>();
            updateWrapper.set(SiteDO::getUvCount, onlineVisitors);
            updateWrapper.eq(SiteDO::getId, siteDO.getId());
            siteService.update(new SiteDO(), updateWrapper);
        }
    }

    public void deleteStatHistory() {
        OffsetDateTime now = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toOffsetDateTime();
        visitTrendService.deleteBeforeDateString(
                now.minusDays(AccessTrendDO.DATA_MAX_DAY_FOR_DAY).format(DAY_FORMATTER),
                AccessTrendDO.PERIOD_DAY);
        visitTrendService.deleteBeforeDateString(
                now.minusDays(AccessTrendDO.DATA_MAX_DAY_FOR_HOUR).format(HOUR_FORMATTER),
                AccessTrendDO.PERIOD_HOUR);
        visitTrendService.deleteBeforeDateString(
                now.minusDays(AccessTrendDO.DATA_MAX_DAY_FOR_MINUTE).format(MINUTE_FORMATTER),
                AccessTrendDO.PERIOD_MINUTE);
        visitPageService.deleteBeforeDateString(now.minusDays(AccessStatisticsDO.DATA_MAX_DAY).format(DAY_FORMATTER));
        visitStatService.deleteBeforeDateString(now.minusDays(AccessStatisticsDO.DATA_MAX_DAY).format(DAY_FORMATTER));
        visitLogService.deleteBeforeDate(now.minusDays(AccessLogDO.DATA_MAX_DAY));
    }

    public void updateVisitStat(Integer type, OffsetDateTime startOfDay) {
        String dateString = startOfDay.format(DAY_FORMATTER);
        // 删除原有统计数据
        visitStatService.deleteByDateString(dateString, type);
        List<AccessStatisticsDO> list = visitLogService.statByType(type, startOfDay, startOfDay.plusDays(1));
        list.forEach(bean -> {
            bean.setId(snowflakeSequence.nextId());
            bean.setDateString(dateString);
            bean.setStatType(type);
        });
        visitStatService.saveBatch(list, BATCH_SIZE);
    }

    public void updateVisitPageByUrl(OffsetDateTime startOfDay) {
        String dateString = startOfDay.format(DAY_FORMATTER);
        // 删除原有统计数据
        visitPageService.deleteByDateString(dateString, AccessPageDO.TYPE_VISITED_URL);
        List<AccessPageDO> list = visitLogService.statUrl(startOfDay, startOfDay.plusDays(1));
        list.forEach(bean -> {
            bean.setId(snowflakeSequence.nextId());
            bean.setDateString(dateString);
            bean.setAccessType(AccessPageDO.TYPE_VISITED_URL);
        });
        visitPageService.saveBatch(list, BATCH_SIZE);
    }

    public void updateVisitPageByEntryUrl(OffsetDateTime startOfDay) {
        String dateString = startOfDay.format(DAY_FORMATTER);
        // 删除原有统计数据
        visitPageService.deleteByDateString(dateString, AccessPageDO.TYPE_ENTRY_URL);
        List<AccessPageDO> list = visitLogService.statEntryUrl(startOfDay, startOfDay.plusDays(1));

        list.forEach(bean -> {
            bean.setId(snowflakeSequence.nextId());
            bean.setDateString(dateString);
            bean.setAccessType(AccessPageDO.TYPE_ENTRY_URL);
        });
        visitPageService.saveBatch(list, BATCH_SIZE);
    }

    public void updateVisitTrendByMonth(OffsetDateTime startOfMonth) {
        String dateString = startOfMonth.format(MONTH_FORMATTER);
        // 删除原有统计数据
        visitTrendService.deleteByDateString(dateString);
        List<AccessTrendDO> list = visitTrendService.statByMonth(startOfMonth, startOfMonth.plusMonths(1));
        updateVisitTrend(list, dateString, AccessTrendDO.PERIOD_MONTH);
    }

    public void updateVisitTrendByDay(OffsetDateTime startOfDay) {
        String dateString = startOfDay.format(DAY_FORMATTER);
        // 删除原有统计数据
        visitTrendService.deleteByDateString(dateString);
        List<AccessTrendDO> list = visitLogService.statTrend(startOfDay, startOfDay.plusDays(1));
        updateVisitTrend(list, dateString, AccessTrendDO.PERIOD_DAY);
    }

    public void updateVisitTrendByHour(OffsetDateTime startOfHour) {
        String dateString = startOfHour.format(HOUR_FORMATTER);
        // 删除原有统计数据
        visitTrendService.deleteByDateString(dateString);
        List<AccessTrendDO> list = visitLogService.statTrend(startOfHour, startOfHour.plusHours(1));
        updateVisitTrend(list, dateString, AccessTrendDO.PERIOD_HOUR);
    }

    public void updateVisitTrendByMinute(OffsetDateTime startOfMinute) {
        String dateString = startOfMinute.format(MINUTE_FORMATTER);
        // 删除原有统计数据
        visitTrendService.deleteByDateString(dateString);
        List<AccessTrendDO> list = visitLogService.statTrend(startOfMinute, startOfMinute.plusMinutes(10));
        updateVisitTrend(list, dateString, AccessTrendDO.PERIOD_MINUTE);
    }

    private void updateVisitTrend(List<AccessTrendDO> list, String dateString, Short period) {
        list.forEach(bean -> {
            bean.setId(snowflakeSequence.nextId());
            bean.setDateString(dateString);
            bean.setStatPeriod(period);
        });
        visitTrendService.saveBatch(list, BATCH_SIZE);
    }

    /**
     * 访问日志插入程序是否繁忙
     */
    private static final AtomicBoolean IS_INSERT_BUSY = new AtomicBoolean(false);

    /**
     * 访问缓存
     */
    private List<AccessLogDO> visitLogCache = Collections.synchronizedList(new LinkedList<>());


    public AccessService(AccessLogService visitLogService, AccessStatisticsService visitStatService,
                         AccessPageService visitPageService, AccessTrendService visitTrendService,
                         GlobalConfigService globalService, SiteService siteService, TenantFrameworkService tenantFrameworkService,
                         TenantProperties tenantProperties, SnowflakeSequence snowflakeSequence) {
        this.visitLogService = visitLogService;
        this.visitStatService = visitStatService;
        this.visitPageService = visitPageService;
        this.visitTrendService = visitTrendService;
        this.globalService = globalService;
        this.snowflakeSequence = snowflakeSequence;
        this.siteService = siteService;
        this.tenantFrameworkService = tenantFrameworkService;
        this.tenantProperties = tenantProperties;
    }

    private final AccessLogService visitLogService;
    private final AccessStatisticsService visitStatService;
    private final AccessPageService visitPageService;
    private final AccessTrendService visitTrendService;
    private final GlobalConfigService globalService;
    private final SnowflakeSequence snowflakeSequence;

    private final SiteService siteService;
    private final TenantFrameworkService tenantFrameworkService;
    private final TenantProperties tenantProperties;
}
